// 有限状态机的简易实现 tokenize
// 定义状态机的状态
const State = {
  initial:1, // 初始状态
  tagOpen:2, // 标签开始状态
  tagName:3,  // 标签名称状态
  text:4,  // 文本状态
  tagEnd:5, // 结束标签状态
  tagEndName:6 // 结束标签名称状态
}
// 一个辅助函数，用于判断是否是字符串
function isAlpha(char){
  return char >= 'a' && char <= 'z' || char >= 'A' && char <= 'Z'
}
// 接收模板字符串作为参数，并将模板切割为 Token 返回
function tokenize(str){
  // 状态机的当前状态：初始状态
  let currentState = State.initial
  // 用于缓存字符
  const chars = []
  // 生成的 Token 会存储到 tokens 数组中，并作为函数的返回值返回
  const tokens = []
  // 使用 while 循环开启自动机，只要模板字符串没有被消费尽，自动机就会一直运行
  while(str){
    // 查看第一个字符，注意，这里只是查看，没有消费字符
    const char = str[0]
    // Switch 匹配当前状态
    switch(currentState){
      // 状态机当前初始状态
      case State.initial:
        // 遇到字符 < 这里主要判断是 标签 还是 文本
        if(char === '<'){
          //1. 状态机切换到标签开始状态  
          currentState = State.tagOpen
          //2. 消费字符 <
          str = str.slice(1)
        }else if(isAlpha(char)){
          //1. 遇到字母，切换到文本状态
          currentState = State.text
          //2. 将当前字母缓存到 chars 数组中
          chars.push(char)
          //3. 消费当前字符
          str = str.slice(1)
        }
        break
      // 状态机当前状态处于标签开始状态
      case State.tagOpen:
        if(isAlpha(char)){
          //1. 遇到字母，切换到标签名称状态
          currentState = State.tagName
          //2. 将当前字符缓存到 chars 数组中
          chars.push(char)
          //3. 消费当前字符
          str = str.slice(1)
        }else if(char === '/'){  
          //1. 遇到字符 / ， 切换到结束标签状态 </div> /
          currentState = State.tagEnd
          //2. 消费字符
          str = str.slice(1)
        }
        break
      case State.tagName:
        if(isAlpha(char)){
          // 遇到字母， 由于当前处于标签名称状态，所以不需要切换状态，需要将字符缓存到 chars 数组中 <div>  div
          chars.push(char)
          str = str.slice(1)
        }else if(char === '>'){
          // 切换到标签初始状态
          currentState = State.initial
          // 同时创建一个标签 Token， 并添加到 tokens 数组中， 此时 chars 数组中缓存的字符 就是标签名称
          tokens.push({
            type:'tag',
            name:chars.join('')
          })
          // chars 数组内容已经被消费，清空
          chars.length = 0
          // 同时消费当前字符 >
          str = str.slice(1)
        }
        break
      case State.text:
        if(isAlpha(char)){
          // 遇到字母，保持状态不变，当应该将当前字符缓存到 chars 数组
          chars.push(char)
          str = str.slice(1)
        }else if(char === '<'){
          currentState = State.tagOpen
          // 从文本状态 ===> 标签开始状态，此时应该创建文本 Token， 并添加到 tokens 数组中， 注意此时 chars 数组中的字符就是 文本内容
          tokens.push({
            type:'text',
            content:chars.join('')
          })
          chars.length = 0
          str= str.slice(1)
        }
        break
      case State.tagEnd:
        if(isAlpha(char)){
          // 遇到字母 切换到结束名称状态  如 </div> 开始走 div
          currentState = State.tagEndName
          chars.push(char)
          str = str.slice(1)
        }
        break
      case State.tagEndName:
        if(isAlpha(char)){
          // 遇到字母 不切换状态
          chars.push(char)
          str = str.slice(1)
        }else if(char === '>'){
          // 切换会初始状态
          currentState = State.initial
          // 从结束标签名称状态 ===> 初始状态， 应该保存结束标签名称 Token， 注意 此时 chars 数组中 缓存的内容就是 标签名称
          tokens.push({
            type:'tagEnd',
            name:chars.join('')
          })
          chars.length = 0
          str = str.slice(1)
        }
        break
    }
  }
  // 最后 返回 tokens
  return tokens
}

// 上面的函数 没有处理 数字的情况
console.log(tokenize(`<p>vue</p>`)); // 正常来说这里得到的结果应该是 [{type:'tag',name:'p'},{type:'text',name:'vue'},{type:tagEnd,name:'p'}]

// 优化后的 parser 增加了 栈队列, 当前 无法处理 自闭 和 标签 以及 数值
function parser(res){
  // 首先对模板进行表计划，得到 tokens
  const tokens = tokenize(res)
  // 创建 Root 根节点
  const root = {
    type:'Root',
    children:[]
  }
  // 创建 elementStack 栈， 期初只有 Root 根节点
  const elementStack = [root]
  // 开启一个 while 循环扫描 tokens， 知道所有 token 都被 扫描完为止
  while(tokens.length){
    // 获取当前栈顶节点 作为父节点 parent
    const parent = elementStack[elementStack.length -1]
    // 当前扫描的 token
    const t = tokens[0]
    switch(t.type){
      case 'tag':
        // 如果当前 Token 是开始标签，则创建 Element 类型的 AST 节点
        const elementNode = {
          type:'Element',
          tag:t.name,
          children:[]
        }
        // 将其添加到父级节点的 children 中
        parent.children.push(elementNode)
        // 将当前节点压入栈
        elementStack.push(elementNode)
        break;
      case 'text':
        // 如果当前 token 是文本， 则创建 text 类型的 AST 节点
        const textNode = {
          type:'Text',
          content:t.content
        }
        // 将其添加到父节点的 children 中
        parent.children.push(textNode)
        break;
      case 'tagEnd':
        // 遇到结束标签， 将栈顶节点弹出
        elementStack.pop()
        break
    }
    // 消费已经通过扫描的 token
    tokens.shift()
  }
  // 最后返回 AST 
  return root
}

const ASTSTR = `<div><p>vue</p><div>react</div></div>`
console.log(dump(parser(ASTSTR)));

// dump 工具函数， 用来进行 AST 节点的深度遍历访问 及打印
function dump(node,indent = 0){
  // 节点类型
  const type = node.type
  // 如果是根节点，则没有描述，如果是 Element 类型的节点，则使用 node.tag 作为节点的描述，如果是 Text 类型的节点，则使用 node.context 作为节点的描述
  const desc = node.type === 'Root' ? "" : node.type === 'Element' ? node.tag : node.content
  // 打印节点的类型和 描述信息
  console.log(`${'-'.repeat(indent)}${type}: ${desc}`);
  // 递归的打印子节点
  if(node.children){
    node.children.forEach(n => dump(n,indent + 2))
  }
}


// 转换函数的 简单原理
function transform(ast){
  // 存储上下文信息的对象
  const context = { 
    // 用来存储当前正在转换的节点
    currentNode:null,
    // 用来存储当前节点在父节点的 chilren 中的位置索引
    childIndex:0,
    // 用来存储当前转换节点的父节点
    parent:null,
    // 节点替换 用来替换当前正在转换的节点 context 这个对象会 通过递归透传， 所有子节点中 都可以通过 context对象访问这个方法
    replaceNode(node){
      context.currentNode = node
      context.parent.children[context.childIndex] = node
    },
    // 删除节点
    removeNode(){
      if(context.parent){
        // 调用数组的 splice 方法， 根据当前节点的索引 删除当前节点
        context.parent.children.splice(context.childIndex,1)
        // 将 context.currentNode 置空
        context.currentNode = null
      }
    },
    nodeTransforms:[
      transformElement,// 转换节点类型函数
      transformText,// 转换文本类型的函数
    ]
  }
  traverseNode(ast,context)
}

// 工作流的实现 只是包含了 进入阶段的工作流， 实际上还要包括退出阶段的工作流， 转换函数返回另一个函数，该函数即作为退出阶段的回调函数
function traverseNode(ast,context){
  // 设置当前转换的节点信息
  context.currentNode = ast
  const transforms = context.nodeTransforms
  for(let i =0;i<transforms.length;i++){
    transforms[i](context.currentNode,context)
  }
  const children = context.currentNode.children
  if(children){
    for(let i = 0; i < children.length ; i++){
      // 递归调用 traverseNode 转换子节点之前， 将当前节点设置为父节点
      context.parent = context.currentNode
      // 设置位置索引
      context.childIndex = i
      // 递归调用时， 将 context 透传
      traverseNode(children[i],context)
    }
  }
}